﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ZOIL.Framework.Core;
using ZOIL.Framework.Presentation;

namespace ZOIL.Library.Behaviors
{
    [Export(typeof(PluginObjectManipulationBehavior))]
    public class TouchObjectManipulationBehavior : PluginObjectManipulationBehavior
    {
        #region private members

        private static readonly Dictionary<int, Dictionary<object, object>> TouchDeviceUserData = new Dictionary<int, Dictionary<object, object>>();

        private static readonly Dictionary<ZComponent, ZObjectManipulationInformation> ElementsBeingManipulated = new Dictionary<ZComponent, ZObjectManipulationInformation>();

        #endregion

        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.IsManipulationEnabled = true;

            //ContactTapGesture was previously implemented for surface handler
            AssociatedObject.ManipulationStarting += AssociatedObjectManipulationStarting;
            AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta;
            AssociatedObject.ManipulationInertiaStarting += AssociatedObjectManipulationInertiaStarting;
            AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted;
            AssociatedObject.TouchDown += AssociatedObject_TouchDown;
        }

        protected override void OnDetaching()
        {
            //ContactTapGesture was previously implemented for surface handler
            AssociatedObject.ManipulationStarting -= AssociatedObjectManipulationStarting;
            AssociatedObject.ManipulationDelta -= AssociatedObjectManipulationDelta;
            AssociatedObject.ManipulationInertiaStarting -= AssociatedObjectManipulationInertiaStarting;
            AssociatedObject.ManipulationCompleted -= AssociatedObjectManipulationCompleted;
            AssociatedObject.TouchDown -= AssociatedObject_TouchDown;

            base.OnDetaching();
        }

        #region Eventhandler

        private static readonly object ClickZoomTimestampKey = new object();
        private static readonly object ClickZoomSourceKey = new object();
        private static readonly object ClickZoomSenderKey = new object();
        private static readonly object ClickZoomInitialPoint = new object();

        public static void OnTouchDown(Object sender, TouchEventArgs e)
        {
            var id = e.TouchDevice.Id;
            if (!TouchDeviceUserData.ContainsKey(id))
                TouchDeviceUserData.Add(id, new Dictionary<object, object>());

            var userData = TouchDeviceUserData[id];

            userData[ClickZoomTimestampKey] = Environment.TickCount;
            userData[ClickZoomSourceKey] = e.Source as FrameworkElement;
            userData[ClickZoomSenderKey] = sender as FrameworkElement;
            userData[ClickZoomInitialPoint] = e.TouchDevice.GetTouchPoint(null).Position;

            //e.Handled = true;
        }

        #endregion

        #region event handling

        void AssociatedObjectManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            var comp = e.OriginalSource as ZComponent;

            if (comp == null)
                return;

            var info = new ZObjectManipulationInformation(comp)
            {
                ElementBeingDragged = ZBehavior.FindCanvasChild(sender as UIElement)
            };

            //info.OriginalPositionOnWindow = e.Device.GetPosition(null);

            info.OriginalFrameworkElement = (e.OriginalSource as FrameworkElement ?? info.ElementBeingDragged as FrameworkElement);

            // TODO: replace using TransformToAncestor()
            info.ParentLandscapes = ZTreeHelper.TryFindParents<ZInformationLandscape>(comp);
            info.Left = Canvas.GetLeft(comp);
            info.Top = Canvas.GetTop(comp);

            info.Touches.Add(e.Device as TouchDevice);

            info.OriginalPositionOnCanvas = new Point(info.Left, info.Top);

            info.LastManipulationOrigin = new Point();

            var center = comp.TranslatePoint(new Point(comp.Width / 2, comp.Height / 2), comp.ParentInformationLandscape.TransformationBorder);
            info.Center = center;

            ElementsBeingManipulated.Add(comp, info);

            e.ManipulationContainer = comp.ParentInformationLandscape.TransformationBorder;
            comp.IsBeingManipulated = true;
            e.Handled = true;
        }

        void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            if (!IsInertiaEnabled && e.IsInertial)
                e.Complete();

            var component = e.OriginalSource as ZComponent;

            if (component == null)
                return;

            e.Handled = true;

            // TODO: dragging works well with following code, without own dragging method --> 
            // evaluate flag for dragging true/false somewhere below
            var info = ElementsBeingManipulated[component];

            //if (info.LastManipulationOrigin == e.ManipulationOrigin && e.DeltaManipulation.Translation.X == 0.0 && e.DeltaManipulation.Translation.Y == 0.0 && e.DeltaManipulation.Rotation == 0.0 && e.DeltaManipulation.Expansion.X == 0.0)
            //    return;

            if (IsResizable || IsDraggable)
            {
                #region Fire Drag Events, TODO: extract this to extra method

                //if (ZObjectBlubAndBinDragDropBehavior.GetIsDraggable(comp) && e.ScaleDelta == 1 && e.CumulativeTranslation.Length > 10)
                if (IsDraggable && e.DeltaManipulation.Scale.X == 1 && e.DeltaManipulation.Scale.Y == 1 && Math.Abs(e.CumulativeManipulation.Translation.X) > 10 && Math.Abs(e.CumulativeManipulation.Translation.Y) > 10)
                {
                    //if we are not dragging yet fire drag started event and set is dragging to true
                    if (!IsDragging)
                    {
                        StartDrag(new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y));
                        IsDragging = true;
                    }
                    //otherwise fire only drag delta event
                    else
                    {
                        if (info.DeltaFired)
                        {
                            info.DeltaFired = false;
                        }
                        else
                        {
                            info.DeltaFired = true;
                            //Raise DragDeltaEvent
                            ChangeDrag(new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y));
                        }
                    }
                }
                else
                {
                    IsDragging = false;
                }

                #endregion
            }

            info.LastManipulationOrigin = e.ManipulationOrigin;
            var point = e.ManipulationOrigin - e.DeltaManipulation.Translation;
            var center = info.Center - point;

            var canMove = IsDraggable;
            var canRotate = IsRotatable;
            var canResize = IsResizable;

            var angle = canRotate ? NormalizeAngle(e.DeltaManipulation.Rotation) : 0.0;
            var scale = canResize ? e.DeltaManipulation.Scale.X : 1.0;
            var translation = canMove ? e.DeltaManipulation.Translation : new Vector(0, 0);

            //CheckTouchesForOutOfBoundsTracking(component, ref ignoreXTranslation, ref ignoreYTranslation, ref ignoreRotation);

            if (canMove && angle != 0)
            {
                var v2 = RotateVector(center, angle);
                translation += v2 - center;
            }

            if (canMove && scale != 1)
            {
                var v3 = center * scale;
                translation += v3 - center;
            }

            e.Handled = true;

            var boundary = info.ParentLandscapes.First().EnableBoundaries ? ConstrainDelta(component, ref translation, ref angle, ref scale, info.Center, new Rect(component.ParentInformationLandscape.TransformationBorder.RenderSize)) : Boundary.None;

            if (e.IsInertial)
            {
                var containingRect = new Rect(component.ParentInformationLandscape.TransformationBorder.RenderSize);

                var shapeBounds = component.RenderTransform.TransformBounds(new Rect(component.RenderSize));

                var t = !containingRect.Contains(shapeBounds);

                Console.WriteLine("T: {0}", t);

                if (e.IsInertial && boundary != Boundary.None)
                {
                    //Report that we have gone over our boundary 
                    e.ReportBoundaryFeedback(e.DeltaManipulation);
                    // comment out this line to see the Window 'shake' or 'bounce' 
                    // similar to Win32 Windows when they reach a boundary; this comes for free in .NET 4                
                    e.Complete();
                }
            }

            if (angle != 0)
            {
                component.Angle += angle;
            }

            if (scale != 1)
            {
                component.Width *= scale;
                component.Height *= scale;
            }

            if (translation.LengthSquared > 0)
            {
                var centerPoint = new Point(info.Center.X + translation.X, info.Center.Y + translation.Y);
                var t = GetOffset(centerPoint, angle, component.ActualWidth, component.ActualHeight, component.RenderTransformOrigin);

                Canvas.SetLeft(component, t.X);
                Canvas.SetTop(component, t.Y);

                info.Center = centerPoint;
            }
        }

        void AssociatedObjectManipulationInertiaStarting(object sender, ManipulationInertiaStartingEventArgs e)
        {
            var component = e.OriginalSource as ZComponent;

            if (component == null)
                return;

            e.Handled = true;

            var info = ElementsBeingManipulated[component];

            var initialVelocity = e.InitialVelocities.LinearVelocity;

            if (info.BounceBorder != Boundary.None)
            {
                if ((info.BounceBorder & Boundary.Top) == Boundary.Top || (info.BounceBorder & Boundary.Bottom) == Boundary.Bottom)
                {
                    initialVelocity.X = -initialVelocity.X;
                    initialVelocity.Y = -initialVelocity.Y;
                    e.TranslationBehavior.DesiredDisplacement = 200;
                }

                if ((info.BounceBorder & Boundary.Left) == Boundary.Left || (info.BounceBorder & Boundary.Right) == Boundary.Right)
                {
                    initialVelocity.X = -initialVelocity.X;
                    initialVelocity.Y = -initialVelocity.Y;
                    e.TranslationBehavior.DesiredDisplacement = 200;
                }
            }
            else
            {
                // http://msdn.microsoft.com/en-us/library/ee649090.aspx
                // Decrease the velocity of the Rectangle's movement by 
                // 10 inches per second every second.
                // (10 inches * 96 pixels per inch / 1000ms^2)
                e.TranslationBehavior.DesiredDeceleration = 10.0 * 96.0 / (1000.0 * 1000.0);

                // Decrease the velocity of the Rectangle's resizing by 
                // 0.1 inches per second every second.
                // (0.1 inches * 96 pixels per inch / (1000ms^2)
                e.ExpansionBehavior.DesiredDeceleration = 0.1 * 96 / (1000.0 * 1000.0);

                // Decrease the velocity of the Rectangle's rotation rate by 
                // 2 rotations per second every second.
                // (2 * 360 degrees / (1000ms^2)
                e.RotationBehavior.DesiredDeceleration = 720 / (1000.0 * 1000.0);
            }

            info.BounceBorder = Boundary.None;
        }

        void AssociatedObjectManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {
            var component = e.OriginalSource as ZComponent;

            if (component == null)
                return;

            component.IsBeingManipulated = false;
            if (ElementsBeingManipulated.ContainsKey(component))
            {
                var info = ElementsBeingManipulated[component];
                //Raise DragFinishedEvent and set IsDragging to false

                //TODO: e.OriginX/e.OriginY == e.ManipulationOrigin.X/Y ?

                FinishDrag(info.ComponentBeingManipulated, new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y));

                ElementsBeingManipulated.Remove(component);
            }
        }

        void AssociatedObject_TouchDown(object sender, TouchEventArgs e)
        {
            var id = e.TouchDevice.Id;
            if (!TouchDeviceUserData.ContainsKey(id))
                TouchDeviceUserData.Add(id, new Dictionary<object, object>());

            var userData = TouchDeviceUserData[id];

            userData[ClickZoomTimestampKey] = Environment.TickCount;
            userData[ClickZoomSourceKey] = e.Source as FrameworkElement;
            userData[ClickZoomSenderKey] = sender as FrameworkElement;
            userData[ClickZoomInitialPoint] = e.TouchDevice.GetTouchPoint(null).Position;

            //e.Handled = true;
        }

        #endregion

        #region Utils

        private static Rect GetItemBounds(Size componentSize, Point renderTransformOrigin, Vector translation, double angle, double scale, Point center)
        {
            var renderOrigin = new Point(componentSize.Width * renderTransformOrigin.X * scale, componentSize.Height * renderTransformOrigin.Y * scale);
            var offset = GetOffset(center + translation, angle, componentSize.Width * scale, componentSize.Height * scale, renderTransformOrigin);

            var renderMatrix = GetRenderMatrix(renderOrigin, offset, angle);

            var itemBounds = new MatrixTransform(renderMatrix).TransformBounds(new Rect(new Size(componentSize.Width * scale, componentSize.Height * scale)));
            return itemBounds;
        }

        private static Boundary ConstrainDelta(ZComponent component, ref Vector translation, ref double angle, ref double scale, Point center, Rect containerBounds)
        {
            if (translation.LengthSquared == 0 && scale == 1 && angle == 0)
                return Boundary.None;

            var boundary = Boundary.None;
            var itemBounds = GetItemBounds(component.RenderSize, component.RenderTransformOrigin, translation, component.Angle + angle, scale, center);

            if (!containerBounds.Contains(itemBounds))
            {
                var edges = new[] { itemBounds.TopLeft, itemBounds.TopRight, itemBounds.BottomRight, itemBounds.BottomLeft };
                foreach (var edge in edges)
                {
                    if (!containerBounds.Contains(edge))
                    {
                        if (edge.X < 0)
                        {
                            boundary |= Boundary.Left;
                        }
                        else if (edge.X > containerBounds.Right)
                        {
                            boundary |= Boundary.Right;
                        }
                        if (edge.Y < 0)
                        {
                            boundary |= Boundary.Top;
                        }
                        else if (edge.Y > containerBounds.Bottom)
                        {
                            boundary |= Boundary.Bottom;
                        }
                    }
                }

                var vector = default(Vector);
                var num = 0.0;
                var num2 = 0;

                if (translation.LengthSquared > 0.0)
                {
                    num2 = (int)translation.Length + 1;
                }
                else
                {
                    if (angle != 0.0)
                    {
                        num2 = (int)(2.0 * Math.Abs(angle)) + 1;
                    }
                }
                if (num2 > 0)
                {
                    var vector2 = new Vector(translation.X / num2, translation.Y / num2);
                    var num3 = angle / num2;
                    for (var i = 0; i < num2; i++)
                    {
                        if (containerBounds.Contains(GetItemBounds(component.RenderSize, component.RenderTransformOrigin, vector + vector2, component.Angle + num + num3, 1, center)))
                        {
                            vector += vector2;
                            num += num3;
                        }
                        else
                        {
                            i--;
                            if (vector2.X != 0.0)
                                vector2.X = 0.0;
                            else
                            {
                                if (vector2.Y == 0)
                                    break;
                                vector2.Y = 0.0;
                                if (translation.X != 0)
                                    vector2.X = translation.X / num2;
                            }
                        }
                    }
                }
                scale = 1;
                angle = num;
                translation = vector;
            }

            return boundary;
        }

        internal static Vector GetOffset(Point center, double angle, double width, double height, Point renderTransformOrigin)
        {
            var renderOrigin = new Point(width * renderTransformOrigin.X, height * renderTransformOrigin.Y);
            var point = GetRenderMatrix(renderOrigin, new Vector(0.0, 0.0), angle).Transform(new Point(width * 0.5, height * 0.5));
            return center - point;
        }

        private static Matrix GetRenderMatrix(Point renderOrigin, Vector offset, double rotation)
        {
            var identity = Matrix.Identity;
            identity.RotateAt(rotation, renderOrigin.X, renderOrigin.Y);
            identity.Translate(offset.X, offset.Y);
            return identity;
        }

        //static void CheckTouchesForOutOfBoundsTracking(ZComponent component, ref bool ignoreXTranslation, ref bool ignoreYTranslation, ref bool ignoreRotation)
        //{
        //    ZInformationLandscape landscape = component.ParentInformationLandscape;
        //    int num = 0;
        //    foreach (InputDevice current in TouchExtensions.GetInputDevicesCaptured(component))
        //    {
        //        num++;
        //        Point position = current.GetPosition(landscape);
        //        Rect componentRect = new Rect(new Point(Canvas.GetLeft(component), Canvas.GetTop(component)), component.RenderSize);
        //        if (componentRect.Contains(position))
        //        {
        //            ignoreXTranslation = false;
        //            ignoreYTranslation = false;
        //            return;
        //        }
        //        if (landscape != null)
        //        {
        //            ignoreXTranslation = (ignoreXTranslation || position.X < 0.0 || position.X > landscape.ActualWidth);
        //            ignoreYTranslation = (ignoreYTranslation || position.Y < 0.0 || position.Y > landscape.ActualHeight);
        //        }
        //    }
        //    ignoreRotation = ((ignoreXTranslation ^ ignoreYTranslation) && num == 1);
        //}

        private static Vector RotateVector(Vector vector, double degrees)
        {
            var num = degrees * Math.PI / 180;
            var num1 = Math.Sin(num);
            var num2 = Math.Cos(num);
            var x = vector.X * num2 - vector.Y * num1;
            var x1 = vector.X * num1 + vector.Y * num2;
            return new Vector(x, x1);
        }

        static double NormalizeAngle(double angle)
        {
            var num = angle % 360;
            if (num <= 180)
            {
                if (num <= -180)
                {
                    num = num + 360;
                }
            }
            else
            {
                num = num - 360;
            }
            return num;
        }

        #endregion
    }
}
